<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | 数据库快捷操作
// +----------------------------------------------------------------------
namespace Inphp\Core\Db;

use Exception;
use Inphp\Core\Config;
use Inphp\Core\Context;
use Inphp\Core\Db\PDO\Connection;
use Inphp\Core\Db\PDO\Model;
use Inphp\Core\Db\PDO\Query;
use Inphp\Core\Service;
use Inphp\Core\Util\Log;
use Inphp\Core\Util\Str;
use PDO;
use PDOException;

class Db
{
    /** 代表写 **/
    const WRITE = "write";
    /** 代表读 **/
    const READ = "read";

    /**
     * 数据库配置
     * @var array
     */
    private static array $config = [];

    /**
     * 连接对象/连接池
     * @var Connection|array|null
     */
    private static Connection|array|null $connection = null;

    /**
     * 初始化
     */
    public static function init(): void
    {

    }

    /**
     * 获取配置
     * @param string|null $name
     * @param array $defaultValue
     * @return array
     */
    public static function getConfig(?string $name = null, array $defaultValue = []): array
    {
        if (empty(self::$config)) {
            self::$config = Config::get("db");
        }
        return $name ? (self::$config[$name] ?? $defaultValue) : self::$config;
    }

    /**
     * 获取连接池/连接对象
     * @return Connection
     */
    public static function getConnection(): Connection
    {
        if (Service::isCLI()) {
            if (!self::$connection) {
                self::$connection = new Connection(self::getConfig("pdo"));
            }
            return self::$connection;
        } else {
            $myPID = getmypid();
            self::$connection = is_array(self::$connection) ? self::$connection : [];
            if (!isset(self::$connection[$myPID])) {
                self::$connection[$myPID] = new Connection(self::getConfig("pdo"));
            }
            return self::$connection[$myPID];
        }
    }

    /**
     * 获取PDO操作对象
     * @param string $type
     * @return PDO
     */
    public static function getPDO(string $type = Db::READ): PDO
    {
        //如果目前处于事务状态，强制使用事务状态下的PDO对象
        return self::getConnection()->get($type);
    }

    /**
     * 回收PDO操作对象
     * @param PDO $PDO
     * @param string $type
     */
    public static function putPDO(PDO $PDO, string $type = Db::READ): void
    {
        self::getConnection()->put($PDO, $type);
    }

    /**
     * 获取遍历模式
     * @return int
     */
    public static function getFetchModel(): int
    {
        $config = self::getConfig("pdo");
        return $config["fetchModel"] ?? PDO::FETCH_ASSOC;
    }

    /**
     * 获取统一的表前缀
     * @return string
     */
    public static function getTablePrefix(): string
    {
        $config = self::getConfig("pdo", []);
        return $config["tablePrefix"] ?? "";
    }

    /**
     * 获取数据库名称
     * @param string|null $type
     * @return string
     */
    public static function getDatabaseName(?string $type = null): string
    {
        if (is_null($type) && self::inTransaction()) {
            $type = self::WRITE;
        }
        $type = $type == self::WRITE || $type == self::READ ? $type : self::READ;
        return self::getConnection()->getDatabaseName($type);
    }

    /**
     * 开启一个事务，事务仅支持写入
     * @return PDO
     */
    public static function beginTransaction(): PDO
    {
        $pdo = self::inTransaction();
        if (!$pdo) {
            //强制
            $type = self::WRITE;
            $pdo = self::getPDO($type);
            if (!$pdo->inTransaction()) {
                $pdo->beginTransaction();
            }
            //保存到临时上下文，仅在当次请求有效
            Context::set("pdo_{$type}", $pdo);
            Context::set("pdo_{$type}_transaction", true);
            self::debug("开始新事务");
        } else {
            self::debug("事务已开始，不需要重复开始！");
        }
        return $pdo;
    }

    /**
     * 开启一个事务
     * @return PDO
     */
    public static function begin() : PDO
    {
        return self::beginTransaction();
    }

    /**
     * 是否正在事务中
     * @return PDO|bool
     */
    public static function inTransaction(): PDO|false
    {
        //强制为写
        $type = self::WRITE;
        $state = Context::get("pdo_{$type}_transaction", false);
        $pdo = Context::get("pdo_{$type}");
        return $state && $pdo ? $pdo : false;
    }

    /**
     * 事务回滚
     */
    public static function rollback(): void
    {
        if ($pdo = self::inTransaction()) {
            self::debug("事务回滚：结束");
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
        }
        self::endTransaction();
    }

    /**
     * 提交事务
     */
    public static function commit(): void
    {
        if ($pdo = self::inTransaction()) {
            self::debug("事务提交：成功");
            if ($pdo->inTransaction()) {
                $pdo->commit();
            }
        }
        self::endTransaction();
    }

    /**
     * 结束事务
     */
    private static function endTransaction(): void
    {
        //强制为写
        $type = self::WRITE;
        //标记结束
        Context::set("pdo_{$type}_transaction", false);
        //PDO
        $pdo = Context::get("pdo_{$type}");
        if ($pdo) {
            //调试
            self::debug("事务结束，开始回收PDO");
            self::putPDO($pdo, $type);
        } else {
            //调试
            self::debug("事务无效，无需回收PDO");
        }
    }

    /**
     * 简单的查询开始
     * @param string|Query|Model $table
     * @param string $as
     * @return Query
     */
    public static function from(string|Query|Model $table, string $as = ""): Query
    {
        return (new Query())->from($table, $as);
    }

    /**
     * 执行SQL
     * @param string $sql
     * @param array $params
     * @param string $type
     * @param int|null $fetchModel
     * @return array|false
     */
    public static function query(string $sql, array $params = [], string $type = Db::READ, ?int $fetchModel = null, string $for = "select"): array|false
    {
        //清空左右空格
        $sql = trim($sql);
        if (Str::trim($sql) == "") {
            return false;
        }
        $pdo = self::getPDO($type);
        //记录执行日志
        $executeLog = [
            "time"      => date("Y/m/d H:i:s"),
            "sql"       => $sql,
            "params"    => $params,
            "success"   => false,
            "affectRows"=> 0
        ];
        $select = $for === "select" || stripos($sql, "select") === 0;
        $insert = stripos($sql, "insert") === 0;
        //返回数据
        $result = null;
        //设置允许重连的次数
        $reconnectTimes = self::getConfig("pdo")["reconnectTimes"] ?? 5;
        //最后产生的ID
        $lastInsertId = 0;
        while (true) {
            try {
                $stmt = $pdo->prepare($sql);
                if ($stmt->execute(!empty($params) ? $params : null)) {
                    $affectRows = $stmt->rowCount() ?? 0;
                    if ($insert) {
                        $lastInsertId = $pdo->lastInsertId();
                    }
                    $executeLog["success"] = true;
                    $executeLog["affectRows"] = $affectRows;
                    $result = $select ? $stmt->fetchAll($fetchModel ?? self::getFetchModel()) : [$affectRows, $lastInsertId];
                    //游标关闭
                    $stmt->closeCursor();
                    //跳出循环
                    break;
                } else {
                    $code = $stmt->errorCode();
                    throw new PDOException($stmt->errorInfo()[2]."\r\nquery : ".$stmt->queryString."\r\nparams :".json_encode($params, 256), $code);
                }
            } catch (PDOException $exception) {
                if (stripos(strtolower($exception->getMessage()), "server has gone away") !== false && $reconnectTimes > 0) {
                    //需要重连
                    unset($pdo);
                    $pdo = self::getConnection()->reconnect($type);
                    $reconnectTimes--;
                } else {
                    self::saveErrorLog($exception);
                    //保存错误的SQL
                    self::saveErrorLog(join("\r\n", ["sql: {$executeLog['sql']}", "params: ".json_encode($executeLog['params'], 256)])."\r\n");
                    //跳出循环
                    break;
                }
            }
        }
        //保存执行日志
        self::saveExecuteLog($executeLog, $select ? "r" : "cud");
        //自动回收对象
        self::putPDO($pdo, $type);
        //返回数据
        return $result ?? false;
    }

    /**
     * 保存错误日志
     * @param string|Exception $content
     * @param string $name
     */
    public static function saveErrorLog(string|Exception $content, string $name = "pdo"): void
    {
        $config = self::getConfig($name, []);
        $logFile = $config["errorLogFile"] ?? "";
        if (!empty($logFile)) {
            Log::writeToEnd($logFile, $content);
        }
    }

    /**
     * 保存执行日志
     * @param array $content
     * @param string $type
     */
    public static function saveExecuteLog(array $content, string $type = "select"): void
    {
        $config = self::getConfig("pdo", []);
        $dir = $config["executeLogDir"] ?? "";
        if (!empty($dir)) {
            $logFile = $dir."/".date("Ymd")."_{$type}.txt";
            Log::writeToEnd($logFile, json_encode($content, 256));
        }
    }

    /**
     * 输出debug
     * @param string $text
     * @param string $name
     */
    public static function debug(string $text, string $name = "pdo") {
        if ((self::getConfig($name)["debug"] ?? false) && Service::isCLI()) {
            echo $text.PHP_EOL;
        }
    }
}